﻿/*
 * LINQ to LDAP
 * http://linqtoldap.codeplex.com/
 * 
 * Copyright Alan Hatter (C) 2010-2012
 * alan.hatter@gmail.com - http://hattercoding.blogspot.com/
 * 
 * This project is subject to licensing restrictions. Visit http://linqtoldap.codeplex.com/license for more information.
 */

using System;
using System.Collections.Generic;
using System.DirectoryServices.Protocols;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using LinqToLdap.Collections;
using LinqToLdap.Exceptions;

namespace LinqToLdap
{
    /// <summary>
    /// Contains <see cref="IQueryable{T}"/> extension methods
    /// </summary>
    public static class QueryableExtensions
    {
        /// <summary>
        /// Allows you to specify add <see cref="DirectoryControl"/>s to the query.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query.</param>
        /// <param name="controls">The <see cref="DirectoryControl"/>s to use.</param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> or <paramref name="controls"/> is null.
        /// </exception>
        /// <exception cref="FilterException">
        /// Thrown if <paramref name="controls"/> has 0 elements.
        /// </exception>
        /// <returns></returns>
        public static IQueryable<TSource> WithControls<TSource>(this IQueryable<TSource> source, IEnumerable<DirectoryControl> controls)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (controls == null) throw new ArgumentNullException("controls");

            if (!controls.Any()) throw new FilterException("Must specify at least one control when using WithControls.");

            return
                source.Provider.CreateQuery<TSource>(
                    Expression.Call(null,
                                    ((MethodInfo)MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] { typeof(TSource) }),
                                    new[] { source.Expression, Expression.Constant(controls) }));
        }

        /// <summary>
        /// Creates an attribute scoped query using <see cref="AsqRequestControl"/>.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query.</param>
        /// <param name="attributeName">The name of the attribute to which this query will be scoped.</param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        /// <returns></returns>
        public static IQueryable<TSource> ScopedToAttribute<TSource>(this IQueryable<TSource> source, string attributeName)
        {
            return source.WithControls(new[] {new AsqRequestControl(attributeName)});
        }

        /// <summary>
        /// Adds a <see cref="SearchOptionsControl"/> to the search with the specified <paramref name="option"/>.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query.</param>
        /// <param name="option">The <see cref="SearchOption"/> for the search</param>
        /// <returns></returns>
        public static IQueryable<TSource> SearchWith<TSource>(this IQueryable<TSource> source, SearchOption option)
        {
            return source.WithControls(new[] { new SearchOptionsControl(option) });
        }
        
        /// <summary>
        /// Lists found attributes for all entries returned by the query.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query.</param>
        /// <param name="attributes">Specify specific attributes to load</param>
        /// <returns></returns>
        public static IEnumerable<KeyValuePair<string, IEnumerable<KeyValuePair<string, object>>>> ListAttributes<TSource>(
            this IQueryable<TSource> source, params string[] attributes)
        {
            return source.Provider.Execute<IEnumerable<KeyValuePair<string, IEnumerable<KeyValuePair<string, object>>>>>(
                Expression.Call(null,
                                ((MethodInfo) MethodBase.GetCurrentMethod())
                                    .MakeGenericMethod(new[] {typeof (TSource)}),
                                new[] {source.Expression, Expression.Constant(attributes)}));
        }

        /// <summary>
        /// Allows you to specify a custom filter for querying.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query.</param>
        /// <param name="filter">The filter to use for querying</param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="FilterException">
        /// Thrown if <paramref name="filter"/> is null, empty, or white-space.
        /// </exception>
        /// <returns></returns>
        public static IQueryable<TSource> FilterWith<TSource>(this IQueryable<TSource> source, string filter)
        {
            if (source == null) throw new ArgumentNullException("source");

            if (filter.IsNullOrEmpty()) throw new FilterException("Filters cannot be null, empty, or white-space.");

            if (!filter.StartsWith("("))
            {
                filter = "(" + filter + ")";
            }

            return
                source.Provider.CreateQuery<TSource>(
                    Expression.Call(null,
                                    ((MethodInfo) MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] {typeof (TSource)}),
                                    new[] {source.Expression, Expression.Constant(filter)}));
        }

        /// <summary>
        /// Allows you to specify a custom filter for querying.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query.</param>
        /// <param name="filter">The filter to use for querying</param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="FilterException">
        /// Thrown if <paramref name="filter"/> is null, empty, or white-space.
        /// </exception>
        /// <returns></returns>
        public static IQueryable<TSource> Where<TSource>(this IQueryable<TSource> source, string filter)
        {
            return FilterWith(source, filter);
        }

        /// <summary>
        /// Inclides the object category in the filter of a query for a mapped object.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query.</param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="MappingException">
        /// Thrown if no object category has been mapped.
        /// </exception>
        /// <returns></returns>
        [Obsolete("This method is no longer necessary.  Object Category will be included by default if it is mapped.")]
        public static IQueryable<TSource> FilterWithObjectCategory<TSource>(this IQueryable<TSource> source)
        {
            if (source == null) throw new ArgumentNullException("source");

            return
                source.Provider.CreateQuery<TSource>(
                    Expression.Call(null,
                                    ((MethodInfo)MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] { typeof(TSource) }),
                                    new[] { source.Expression }));
        }

        /// <summary>
        /// Inclides the object class in the filter of a query for a mapped object.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query.</param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="MappingException">
        /// Thrown if no object class has been mapped.
        /// </exception>
        /// <returns></returns>
        [Obsolete("This method is no longer necessary.  Object Class will be included by default if it is mapped.")]
        public static IQueryable<TSource> FilterWithObjectClass<TSource>(this IQueryable<TSource> source)
        {
            if (source == null) throw new ArgumentNullException("source");

            return
                source.Provider.CreateQuery<TSource>(
                    Expression.Call(null,
                                    ((MethodInfo)MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] { typeof(TSource) }),
                                    new[] { source.Expression }));
        }

        /// <summary>
        /// When this method is included in a query the object class or the object category will not be used in the filter.
        /// </summary>
        /// <param name="source">The query.</param>
        /// <typeparam name="TSource">The type to query against.</typeparam>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        public static IQueryable<TSource> IgnoreOC<TSource>(this IQueryable<TSource> source)
        {
            if (source == null) throw new ArgumentNullException("source");

            return
                source.Provider.CreateQuery<TSource>(
                    Expression.Call(null,
                                    ((MethodInfo)MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] { typeof(TSource) }),
                                    new[] { source.Expression }));
        }

        /// <summary>
        /// Sorts the elements of a sequence in descending order according to a key and mathcing rule.
        /// </summary>
        /// <param name="source">The query</param>
        /// <param name="keySelector">The property to sort by</param>
        /// <param name="matchingRule">The LDAP matching rule to use for the query</param>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <typeparam name="TKey">The type of the property</typeparam>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/>, <paramref name="matchingRule"/>, or <paramref name="keySelector"/> are null
        /// </exception>
        public static IQueryable<TSource> OrderByDescending<TSource, TKey>(this IQueryable<TSource> source, 
            Expression<Func<TSource, TKey>> keySelector, string matchingRule)
        {
            if (source == null) throw new ArgumentNullException("source");

            if (matchingRule.IsNullOrEmpty()) throw new ArgumentNullException("matchingRule");

            if (keySelector == null) throw new ArgumentNullException("keySelector");

            return
                source.Provider.CreateQuery<TSource>(
                    Expression.Call(null,
                                    ((MethodInfo)MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] { typeof(TSource), typeof(TKey) }),
                                    new[] { source.Expression, keySelector, Expression.Constant(matchingRule) }));
        }

        /// <summary>
        /// Sorts the elements of a sequence in ascending order according to a key and mathcing rule.
        /// </summary>
        /// <param name="source">The query</param>
        /// <param name="keySelector">The property to sort by</param>
        /// <param name="matchingRule">The LDAP matching rule to use for the query</param>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <typeparam name="TKey">The type of the property</typeparam>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/>, <paramref name="matchingRule"/>, or <paramref name="keySelector"/> are null
        /// </exception>
        public static IQueryable<TSource> OrderBy<TSource, TKey>(this IQueryable<TSource> source,
            Expression<Func<TSource, TKey>> keySelector, string matchingRule)
        {
            if (source == null) throw new ArgumentNullException("source");

            if (matchingRule.IsNullOrEmpty()) throw new ArgumentNullException("matchingRule");

            if (keySelector == null) throw new ArgumentNullException("keySelector");

            return
                source.Provider.CreateQuery<TSource>(
                    Expression.Call(null,
                                    ((MethodInfo)MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] { typeof(TSource), typeof(TKey) }),
                                    new[] { source.Expression, keySelector, Expression.Constant(matchingRule) }));
        }

        /// <summary>
        /// Immediately pages the query into a <see cref="ILdapPage{T}"/>.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query</param>
        /// <param name="pageSize">The size of the page</param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// Thrown if <paramref name="pageSize"/> is less than one.
        /// </exception>
        /// <returns></returns>
        public static ILdapPage<TSource> ToPage<TSource>(this IQueryable<TSource> source, int pageSize)
        {
            if (source == null) throw new ArgumentNullException("source");

            if (pageSize < 1) throw new ArgumentException("pageSize must be greater than 0", "pageSize");

            return
                source.Provider.Execute<ILdapPage<TSource>>(
                    Expression.Call(null,
                                    ((MethodInfo) MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] {typeof (TSource)}),
                                    new[] {source.Expression, Expression.Constant(pageSize)}));
        }

        /// <summary>
        /// Immediately pages the query into a <see cref="ILdapPage{T}"/>.
        /// </summary>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <param name="source">The query</param>
        /// <param name="pageSize">The size of the page</param>
        /// <param name="nextPage">The cookie of the next page</param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// Thrown if <paramref name="pageSize"/> is less than one.
        /// </exception>
        /// <exception cref="ArgumentException">
        /// Thrown if <paramref name="nextPage"/> is null.
        /// </exception>
        /// <returns></returns>
        public static ILdapPage<TSource> ToPage<TSource>(this IQueryable<TSource> source, int pageSize, byte[] nextPage)
        {
            if (source == null) throw new ArgumentNullException("source");

            if (pageSize < 1) throw new ArgumentException("pageSize must be greater than 0", "pageSize");

            if (nextPage == null) throw new ArgumentException("nextPage cannot be null when paging", "nextPage");

            return
                source.Provider.Execute<ILdapPage<TSource>>(
                    Expression.Call(null,
                                    ((MethodInfo) MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] {typeof (TSource)}),
                                    new[]
                                        {
                                            source.Expression, Expression.Constant(pageSize), Expression.Constant(nextPage)
                                        }));
        }

        /// <summary>
        /// Performs a select projection for a dynamic query.
        /// </summary>
        /// <param name="source">The query.</param>
        /// <param name="attributes">The requested attributes.</param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException">Thrown if <paramref name="source"/> or <paramref name="attributes"/> is null.</exception>
        public static IQueryable<IDirectoryAttributes> Select(this IQueryable<IDirectoryAttributes> source, params string[] attributes)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (attributes == null) throw new ArgumentNullException("attributes");

            return
                source.Provider.CreateQuery<IDirectoryAttributes>(
                    Expression.Call(null,
                                    (MethodInfo)MethodBase.GetCurrentMethod(),
                                    new[] { source.Expression, Expression.Constant(attributes) }));
        }

        /// <summary>
        /// Pages through all the results and returns them in a <see cref="List{T}"/>.
        /// </summary>
        /// <param name="source">The query</param>
        /// <param name="pageSize">The size of each page</param>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <returns></returns>
        [Obsolete("Use InPagesOf with a page size")]
        public static List<TSource> PageAll<TSource>(this IQueryable<TSource> source, int pageSize)
        {
            var pages= new List<TSource>();

            var page = source.ToPage(pageSize);
            pages.AddRange(page);
            while (page.HasNextPage)
            {
                page = source.ToPage(pageSize, page.NextPage);
                pages.AddRange(page);
            }

            return pages;
        }
        
        /// <summary>
        /// Pages through all the results and returns them in a <see cref="List{T}"/>.
        /// </summary>
        /// <param name="source">The query</param>
        /// <param name="pageSize">The size of each page</param>
        /// <typeparam name="TSource">The type to query against</typeparam>
        /// <exception cref="ArgumentNullException">
        /// Thrown if <paramref name="source"/> is null.
        /// </exception>
        /// <exception cref="ArgumentException">Thrown if <paramref name="pageSize"/> is not greater than 0</exception>
        /// <returns></returns>
        public static List<TSource> InPagesOf<TSource>(this IQueryable<TSource> source, int pageSize)
        {
            if (source == null) throw new ArgumentNullException("source");
            if (pageSize < 1) throw new ArgumentException("pageSize must be greater than 0");
            
            return
                source.Provider.Execute<List<TSource>>(
                    Expression.Call(null,
                                    ((MethodInfo)MethodBase.GetCurrentMethod())
                                        .MakeGenericMethod(new[] { typeof(TSource) }),
                                    new[] { source.Expression, Expression.Constant(pageSize) }));
        }
    }
}